package editorsvg.editor.vista;

import editorsvg.editor.comandos.Comando;
import editorsvg.editor.comandos.ComandoAgrupar;
import editorsvg.editor.comandos.ComandoBajar;
import editorsvg.editor.comandos.ComandoBorrar;
import editorsvg.editor.comandos.ComandoCrearForma;
import editorsvg.editor.comandos.ComandoConvertirEnSimbolo;
import editorsvg.editor.comandos.ComandoDesagrupar;
import editorsvg.editor.comandos.ComandoLlevarAlFondo;
import editorsvg.editor.comandos.ComandoSubir;
import editorsvg.editor.comandos.ComandoTraerAlFrente;
import editorsvg.editor.comandos.GestorComandos;
import editorsvg.editor.documento.AjustesGraficos;
import editorsvg.editor.documento.ConjuntoElementos;
import editorsvg.editor.documento.Documento;
import editorsvg.editor.documento.ElementoEditable;
import editorsvg.editor.documento.ListaElementos;
import editorsvg.editor.documento.ElementoSVG;
import editorsvg.editor.documento.MapaSimbolos;
import editorsvg.editor.documento.Seleccion;
import editorsvg.editor.formas.Forma;
import editorsvg.editor.herramientas.Herramienta;
import editorsvg.editor.herramientas.HerramientaCirculo2;
import editorsvg.editor.herramientas.HerramientaElipse2;
import editorsvg.editor.herramientas.HerramientaLinea;
import editorsvg.editor.herramientas.HerramientaModificacion;
import editorsvg.editor.herramientas.HerramientaPoligono;
import editorsvg.editor.herramientas.HerramientaPolilinea;
import editorsvg.editor.herramientas.HerramientaRectangulo;
import editorsvg.editor.herramientas.HerramientaSeleccion;
import editorsvg.editor.herramientas.HerramientaTraveling;
import editorsvg.editor.herramientas.HerramientaTrayecto;
import editorsvg.editor.herramientas.HerramientaZoom2;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.Dimension2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.KeyStroke;

/**
 * Editor de documentos gráficos.
 *
 * @author Jorge Berjano
 */
public class EditorDocumento extends JPanel {

    private ElementoEditable documento;   
    private ConversorCoordenadas conversor;
    private boolean modoEdicion = true;
    private boolean inicializado;
    private boolean modificado = false;
    private Herramienta herramienta;
    private Seleccion seleccion;
    private GestorComandos gestorComandos;
    private BufferedImage bufferedImage;
    private boolean repintando;
    private final int TOLERANCIA_SELECCION = 3;
    private Observador observador;
    private final double margenLienzo = 100;
    private Dimension dimensionMaxima;
    private boolean forzarRepintadoOpcional;

    public void setObservador(Observador observador) {
        this.observador = observador;
    }

    public interface Observador {

        void modificado();

        void seleccionado(ElementoSVG elemento);
    }

    /** Constructor */
    public EditorDocumento() {
        conversor = new ConversorCoordenadas0();
        conversor.setOrigen(new Point2D.Double(margenLienzo, margenLienzo));

        seleccion = new Seleccion(getConversor());

        herramienta = new HerramientaSeleccion(this);

        gestorComandos = new GestorComandos(this);

        if (modoEdicion) {
            // Se asignan los listeners
            DocumentoMouseListener listener = new DocumentoMouseListener();
            addMouseMotionListener(listener);
            addMouseListener(listener);
            addMouseWheelListener(listener);
        }

        // Listener para detectar el cambio de tamaño del editor de documento
        addComponentListener(new java.awt.event.ComponentAdapter() {

            @Override
            public void componentResized(java.awt.event.ComponentEvent evt) {

                if (!inicializado) {
//                    encajarDocumento();
                    inicializado = true;
                }
                ajustarScroll();
            }
        });

        registrarTeclasAceleradoras();

        crearBufferGrafico();
    }

    private void crearBufferGrafico() {
        dimensionMaxima = Toolkit.getDefaultToolkit().getScreenSize();
        if (dimensionMaxima == null) {
            return;
        }
        bufferedImage = new BufferedImage(dimensionMaxima.width, dimensionMaxima.height, BufferedImage.TYPE_INT_RGB);
    }
        
    /**
     * Registra las combinaciones de teclas que realizan acciones en el editor.
     */
    public void registrarTeclasAceleradoras() {

        JComponent comp = this;

        // Se hace un 'Keyboard Binding' para procesar las acciones de teclado
        KeyStroke ksDelete = KeyStroke.getKeyStroke("DELETE");
        KeyStroke ksBackspace = KeyStroke.getKeyStroke("BACKSPACE");

        Action actionBorrar = new AbstractAction("Borrar") {

            @Override
            public void actionPerformed(ActionEvent evt) {
                ejecutarBorrarElementosSeleccionados();
            }
        };
        Object bindingBorrar = actionBorrar.getValue(actionBorrar.NAME);
        comp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ksDelete, bindingBorrar);
        comp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ksBackspace, bindingBorrar);
        comp.getActionMap().put(bindingBorrar, actionBorrar);
    }

    public void ajustarScroll() {

        Dimension2D tamaño = documento.getTamaño();
        if (tamaño == null) {
            return;
        }
        tamaño.setSize(tamaño.getWidth() + margenLienzo * 2, tamaño.getHeight() + margenLienzo * 2);
        setPreferredSize(conversor.aPixeles(tamaño));
        revalidate();
    }
    
    public final void repintar() { // Este es el atiguo repintar que se usa cuando se modifica el documento
        if (observador != null) {
            observador.modificado();
        }
        repintar(false);
    }

    public void repintar(boolean opcional) {
        if (repintando && (opcional || forzarRepintadoOpcional)) {
            System.out.println("pasando de repintar");
            return;
        }        
        repintando = true;        
        Thread t = new Thread(new Runnable() {

            @Override
            public void run() {
                generarBuffer();
                actualizarVista();
                repintando = false;
            }
        } );
        t.start();
    }

    public void actualizarVista() {
        validate();
        repaint();
    }

    private synchronized void generarBuffer() {
//        System.out.println("Comienzo de pintado en buffer...");
//        long t0 = System.currentTimeMillis();

        Graphics2D graphicsBuffer = bufferedImage.createGraphics();
        graphicsBuffer.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        graphicsBuffer.setColor(documento instanceof Documento ? Color.LIGHT_GRAY : Color.WHITE);
        graphicsBuffer.fillRect(0, 0, dimensionMaxima.width, dimensionMaxima.height);

        graphicsBuffer.transform(getConversor().getAffineTransform());
        Point2D posicionRectanguloVisible =
                getConversor().aUnidadesReales(new Point(-getVisibleRect().x, -getVisibleRect().y));
        graphicsBuffer.translate(posicionRectanguloVisible.getX(), posicionRectanguloVisible.getY());
        graphicsBuffer.translate(margenLienzo, margenLienzo);

        AjustesGraficos ajustes = new AjustesGraficos();
        ajustes.setPinturaRelleno(Color.BLACK);
        // Se pinta el documento en el buffer gráfico
        documento.pintar(graphicsBuffer, ajustes);

//        long t1 = System.currentTimeMillis();
//        System.out.println("tiempo de pintado en buffer: " + (t1 - t0) + " ms");
    }

    @Override
    public void paint(Graphics g) {
        Graphics2D graphics = (Graphics2D) g;
        pintar(graphics);
    }
     
    private synchronized void pintar(Graphics2D graphics) {
        graphics.drawImage(bufferedImage, getVisibleRect().x, getVisibleRect().y, null);                    

        // Se pintan guias en coodrdenadas de pantalla (pixeles)
        getSeleccion().pintarEnPixeles(graphics);
        getHerramienta().pintarEnPixeles(graphics);
    }

    public boolean isForzarRepintadoOpcional() {
        return forzarRepintadoOpcional;
    }

    public void setForzarRepintadoOpcional(boolean forzarRepintadoOpcional) {
        this.forzarRepintadoOpcional = forzarRepintadoOpcional;
    }

    public ConversorCoordenadas getConversor() {
        return conversor;
    }

    public void setConversor(ConversorCoordenadas conversor) {
        this.conversor = conversor;
    }

    public Seleccion getSeleccion() {
        return seleccion;
    }

    public void deshacer() {
        gestorComandos.deshacer();
//        seleccion.limpiar();
//        setRepintar();
    }

    public void rehacer() {
        gestorComandos.rehacer();
        repintar();
    }

    public MapaSimbolos getSimbolos() {
        if (documento instanceof Documento) {
            return ((Documento) documento).getSimbolos();
        } else {
            return null;
        }
    }

    /**
     * Clase que implementa los observadores de eventos de ratón para el documento.
     */
    private class DocumentoMouseListener implements MouseMotionListener, MouseListener, MouseWheelListener {

        @Override
        public void mouseDragged(MouseEvent e) {
            // TODO: pasar a las herramientas el punto convertido a unidades de componente
            if (herramienta.arrastrando(e)) {
                actualizarVista();
            }
        }

        @Override
        public void mouseMoved(MouseEvent e) {
            if (herramienta.moviendo(e)) {
                actualizarVista();
            }
        }

        @Override
        public void mouseClicked(MouseEvent e) {
            if (e.getClickCount() == 2) {
                if (herramienta.dobleClick(e)) {
                    actualizarVista();
                }
            }
        }

        @Override
        public void mousePressed(MouseEvent e) {
            if (herramienta.pulsado(e)) {
                actualizarVista();
            }
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            if (herramienta.liberado(e)) {
                actualizarVista();
            }
        }

        @Override
        public void mouseEntered(MouseEvent e) {
        }

        @Override
        public void mouseExited(MouseEvent e) {
        }
        // El zoom con la rueda se ha eliminado porque no se puede determinar
        // cuando deja de rodar para hacer la actualizacciñn permanente de cordenadas
        @Override
        public void mouseWheelMoved(MouseWheelEvent e) {
//            double pixels = - (double) e.getWheelRotation() * 10;
//            double altoPixels = (double) editorPagina.getHeight();
//            double zoom = pixels > 0 ? 1.0 + pixels / altoPixels : altoPixels / (altoPixels - pixels);
//            conversor.acercarVistaPixeles(zoom);
//            actualizarCoordenadas(true);
        }
    }

    /**
     * Establece el documento a editar.
     */
    public void setDocumento(ElementoEditable documento) {       
        seleccion.limpiar();
        this.documento = documento;
        documento.actualizar();   
        repintar();
    }

    /**
     * Obtiene el documento que se esté editando.
     */
    public ElementoEditable getDocumento() {
        return documento;
    }

    /**
     * Obtiene la herramienta actual. 
     */
    public Herramienta getHerramienta() {
        return herramienta;
    }

    /**
     * Asigna la herramienta actual.
     */
    public void setHerramienta(Herramienta herramienta) {
        this.herramienta = herramienta;
        setCursor(herramienta.getCursor());
        actualizarVista();
    }

//    public void setPanelPropiedades(PanelTablaPropiedades panelPropiedades) {
//        this.panelPropiedades = panelPropiedades;
//    }
    public boolean isModificado() {
        return modificado;
    }

//    public void iniciarCreacionForma(Forma forma) {
//        formaEditada = forma;
//    }
    public void finalizarCreacionForma(Forma forma) {
        if (forma == null) {
            return;
        }
        ComandoCrearForma comando = new ComandoCrearForma(forma, this);
        gestorComandos.ejecutar(comando);
    }

    public ElementoSVG obtenerElementoEn(Point2D posicion) {
        double tolerancia = conversor.aUnidadesReales(TOLERANCIA_SELECCION);
        return documento.obtenerElementoEn(posicion, tolerancia);
    }

    public void seleccionar(ElementoSVG elemento, boolean agregar) {
        getSeleccion().seleccionar(elemento, agregar);

        elemento.setGestorComandos(gestorComandos);

        if (observador != null) {
            observador.seleccionado(elemento);
        }
    }

    public void seleccionar(ListaElementos listaElementos, boolean agregar) {
        getSeleccion().seleccionar(listaElementos, agregar);
    }

    public void limpiarSeleccion() {
        getSeleccion().limpiar();
        if (observador != null) {
            observador.seleccionado(null);
        }
    }

    public boolean haySeleccion() {
        return getSeleccion().haySeleccion();
    }

    public boolean seleccionar(Point2D posicion, boolean agregar) {

        ElementoSVG elemento = obtenerElementoEn(posicion);
        if (elemento == null) {
            limpiarSeleccion();
            return false;
        }
        seleccionar(elemento, agregar);
        return true;
    }

    public boolean seleccionar(Rectangle2D rectangulo, boolean agregar) {
        ListaElementos listaElementos = getDocumento().obtenerElementoEn(rectangulo);
        if (listaElementos == null || listaElementos.getLista().size() == 0) {
            limpiarSeleccion();
            return false;
        }
        seleccionar(listaElementos, agregar);
        return true;
    }

    public void setModificado(boolean modificado) {
        this.modificado = modificado;
    }

    public void activarHerramientaSeleccion() {
        setHerramienta(new HerramientaSeleccion(this));
    }

    public void activarHerramientaTraveling() {
        setHerramienta(new HerramientaTraveling(this));
    }

    public void activarHerramientaZoom() {
        setHerramienta(new HerramientaZoom2(this));
    }

    public void activarHerramientaLinea() {
        setHerramienta(new HerramientaLinea(this));
    }

    public void activarHerramientaCirculo() {
        setHerramienta(new HerramientaCirculo2(this));
    }

    public void activarHerramientaElipse() {
        setHerramienta(new HerramientaElipse2(this));
    }

    public void activarHerramientaRectangulo() {
        setHerramienta(new HerramientaRectangulo(this));
    }

    public void activarHerramientaPolilinea() {
        setHerramienta(new HerramientaPolilinea(this));
    }
    
    public void activarHerramientaPoligono() {
        setHerramienta(new HerramientaPoligono(this));
    }

    public void activarHerramientaTrayecto() {
        setHerramienta(new HerramientaTrayecto(this));
    }
        
    public void activarHerramientaModificacion() {
        setHerramienta(new HerramientaModificacion(this));
    }

    public void seleccionarTodo() {
        seleccion.seleccionar(documento.getListaElementos(), false);
    }

    public void ejecutar(Comando comando) {
        gestorComandos.ejecutar(comando);
    }

    public void ejecutarTraerAlFrenteSeleccion() {
        ejecutar(new ComandoTraerAlFrente(documento, getSeleccion().obtenerCopiaElementos()));

    }

    public void ejecutarLlevarAlFondoSeleccion() {
        ejecutar(new ComandoLlevarAlFondo(documento, getSeleccion().obtenerCopiaElementos()));
    }

    public void ejecutarSubirSeleccion() {
        ConjuntoElementos elementosASubir = documento.puedenSubir(seleccion.obtenerCopiaElementos());
        ejecutar(new ComandoSubir(documento, elementosASubir));
    }

    public void ejecutarBajarSeleccion() {
        ConjuntoElementos elementosABajar = documento.puedenBajar(seleccion.obtenerCopiaElementos());
        ejecutar(new ComandoBajar(documento, elementosABajar));
    }

    /**
     * Ejecuta el comando que borra los elementos que estén seleccionados.
     */
    public void ejecutarBorrarElementosSeleccionados() {
        ConjuntoElementos elementos = seleccion.obtenerCopiaElementos();
        seleccion.limpiar();
        ejecutar(new ComandoBorrar(documento, elementos));
    }

    public void ejecutarAgruparSeleccionados() {
        if (!seleccion.haySeleccion()) {
            return;
        }
        ejecutar(new ComandoAgrupar(documento, seleccion.obtenerCopiaElementos()));
    }

    public void ejecutarDesagruparSeleccionados() {

        ejecutar(new ComandoDesagrupar(documento, seleccion.obtenerCopiaElementos()));
    }

    public void ejecutarConvertirSimboloSeleccionados(String nombre) {
        if (!(documento instanceof Documento)) {
            return;
        }
        ConjuntoElementos elementos = getSeleccion().obtenerCopiaElementos();
        seleccion.limpiar();
        ejecutar(new ComandoConvertirEnSimbolo((Documento) documento, elementos, nombre));
    }

    /**
     * Añade un elemento gráfico al documento.
     */
    public void agregarElemento(ElementoSVG elemento) {
        documento.agregarElemento(elemento);
        setModificado(true);
        elemento.actualizar();
        actualizarVista();
    }

    public void borrarElemento(ElementoSVG elemento) {
        documento.borrarElemento(elemento);
        seleccion.deseleccionar(elemento);
        seleccion.actualizar();
        actualizarVista();
    }

    /**
     * Fuerza el rectangulo de la vista para que se visualice el documento completo.
     */
    public void encajarDocumento() {

        if (getDocumento() == null || getDocumento().getEncuadre() == null) {
            return;
        }
//        int anchoPixels = getVisibleRect().width;
//        int altoPixels = getVisibleRect().height;
//        Rectangle2D extensionDocumento = getDocumento().getEncuadre();
//        if (extensionDocumento.getHeight() / anchoPixels > extensionDocumento.getWidth() / altoPixels) {
//            encajarAlto();
//        } else {
//            encajarAncho();
//        }

        encajarAncho();

        ajustarScroll();
        repintar();
    }

    public void desplazarVistaPixeles(int dx, int dy, boolean ajustando) {
        Rectangle rect = getVisibleRect();
        rect.x += dx;
        rect.y += dy;
        scrollRectToVisible(rect);
    }
    
    public void encuadrarVistaPixels(Rectangle rectangulo) {
        Rectangle2D nuevoRectanguloRealVisible = conversor.aUnidadesReales(rectangulo);
        
        Rectangle rectVisible = getVisibleRect();
        double sx = rectVisible.getWidth() / rectangulo.getWidth();
        double sy = rectVisible.getHeight() / rectangulo.getHeight();
        conversor.zoom(Math.min(sx, sy));
        ajustarScroll();
        
        Rectangle nuevoRectanguloVisible = conversor.aPixeles(nuevoRectanguloRealVisible);
        scrollRectToVisible(nuevoRectanguloVisible);
        
        getSeleccion().actualizar();
    }
    
    public void zoom(double zoom) {
        conversor.zoom(zoom);        
        ajustarScroll();
        getSeleccion().actualizar();
    }
    
    public void zoom(double zoom, Point centro) {
        
        Rectangle rectVisible = getVisibleRect();
        int ancho = (int) (rectVisible.width / zoom);
        int alto = (int) (rectVisible.height / zoom);
        
        Rectangle rect = new Rectangle(centro.x - ancho / 2, centro.y - alto / 2, ancho, alto);
        encuadrarVistaPixels(rect);
        
//        int dx = puntoReferencia.x - (int) rectVisible.getCenterX();
//        int dy = puntoReferencia.y - (int) rectVisible.getCenterY();
//        
//        conversor.zoom(zoom);        
//        ajustarScroll();
//        
//        Rectangle rectVisible = getVisibleRect();
//        rectVisible.translate(dx, dy);
//        scrollRectToVisible(rectVisible);
//        
//        getSeleccion().actualizar();
    }


    /**
     * Fuerza el rectangulo de la vista para que se visualice todo el ancho del documento.
     */
    public void encajarAncho() {
        int anchoPixels = getVisibleRect().width;
        int altoPixels = getVisibleRect().height;
        if (anchoPixels == 0 || altoPixels == 0) {
            return;
        }

        if (getDocumento() == null || getDocumento().getEncuadre() == null) {
            return;
        }

        Rectangle2D extensionDocumento = getDocumento().getEncuadre();

//        int alto = (int) extensionDocumento.getWidth() * altoPixels / anchoPixels;
//
//        double altoSobrante = alto - extensionDocumento.getHeight();
//        if (altoSobrante < 0) {
//            altoSobrante = 0;
//        }
//        desplazarVistaPixeles(
//                (int) extensionDocumento.getX(),
//                (int) (extensionDocumento.getY() - altoSobrante / 2));
        conversor.setZoom(altoPixels / extensionDocumento.getHeight());
//        scrollRectToVisible(rect);

        ajustarScroll();
        repintar();
    }

    /**
     * Fuerza el rectangulo de la vista para que se visualice todo el largo del documento.
     */
    public void encajarAlto() {
        int anchoPixels = getVisibleRect().width;
        int altoPixels = getVisibleRect().height;

        if (anchoPixels == 0 || altoPixels == 0) {
            return;
        }

        if (getDocumento() == null || getDocumento().getEncuadre() == null) {
            return;
        }

        Rectangle2D extensionDocumento = getDocumento().getEncuadre();

        //double ancho = extensionDocumento.getHeight() * getSize().width / getSize().height;
        int ancho = (int) extensionDocumento.getHeight() * anchoPixels / altoPixels;

        double anchoSobrante = ancho - extensionDocumento.getWidth();
        if (anchoSobrante < 0) {
            anchoSobrante = 0;
        }

//        desplazarVistaPixeles(
//                (int) (extensionDocumento.getX() - anchoSobrante / 2),
//                (int) extensionDocumento.getY());
        conversor.setZoom(anchoPixels / extensionDocumento.getWidth());
        ajustarScroll();
        repintar();
    }
}
